Conversation
Expose the AASPE query pipeline via a Model Context Protocol (MCP) server so LLM clients can search and read AAS data without writing raw AASQL. Tools (src/IO.Swagger.Lib.V3/MCP/McpQueryTools.cs): - aas_query / aas_count: slot-based search over shells/submodels; builds AASQL JSON ($and/$or) and returns identifiers. idShortPath without a dot resolves by idShort; numeric eq/ne when the value parses as a number. - aas_get_submodel / aas_get_element: read a submodel or a single SME in compact "value" or full AAS-JSON format. - aas_get_shell: read the AAS (assetInformation + submodel references). Registered as Streamable HTTP transport at /api/v3.0/mcp. Compact per-call console logging via [MCP] lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the initial MCP server with higher-level tools and a reduced
endpoint for weak models, plus query-semantics and robustness fixes.
New tools (McpQueryTools.cs):
- aas_get_shell: read the AAS (assetInformation + submodel references)
- aas_get_product: AAS + values of ALL its submodels in one call;
accepts an AAS id or a submodel id (resolves the shell automatically)
- aas_find_product: search + full product in a single call, so models
that do not chain tool calls still get everything at once
Two endpoints (ServerConfiguration.cs): /mcp exposes all tools; the new
/mcp-basic exposes only aas_find_product (for weak models that otherwise
pick the wrong tool or stop after the first call). Implemented via MCP
request filters that inspect the request path.
Query/serialization refinements:
- format=value|full on the get_*/find tools (value-only or full AAS-JSON
incl. semanticId/valueType/qualifiers)
- numeric eq/ne when the value parses as a number (NumberStyles.Float so
"1,32" stays a string); idShortPath without a dot resolves by idShort;
empty sme field defaults to "value"
- validation errors returned as readable {error} results instead of
opaque failures; NotFoundException handled as {found:false}
- nextStep hints on aas_query/aas_count; compact [MCP] console logging
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Small models (e.g. Qwen2.5:7b) call the tool but fill the conditions[]
schema incorrectly (path in field, invented operators/fields). Give them
a tool with flat parameters and route models to an endpoint matching
their capability.
- aas_find_product_simple(idShort, value, format): finds a product by a
single property (idShort = value) and returns the whole product, with
no scope/op/conditions to fill wrong. Shares FindProductCore with
aas_find_product.
- Three endpoints via the path-based request filter:
/mcp all tools (strong models that chain)
/mcp-basic aas_find_product (conditions; mid models, complex search)
/mcp-simple aas_find_product_simple only (weak models)
Tool visibility and call rejection are driven by the request path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Small models (7-8B) confabulate when given the full product (many submodels with bulky CAD/BoM/documentation), inventing fields that are not in the data. Make the simple tool's output minimal and focused. - Drop the format parameter: aas_find_product_simple always returns the compact value form (full overwhelms small models). - coreOnly filter: BuildProductObject/FindProductCore gain a coreOnly flag; the simple tool sets it to skip noisy file/doc submodels (CAD, BoM_SpareParts, HandoverDocumentation) by idShort keyword, returning only Nameplate / TechnicalData / CarbonFootprint. Smaller payload also helps fit small context windows. The full toolset (aas_find_product, aas_get_product) is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Driven by a real at-scale scenario (filtering power supplies and building a table) that needed ~20 query+get round-trips. Combine search with projection and add list/batch operations. - aas_query select=[idShortPaths]: return a row of those field values per hit (projection/table) instead of only identifiers; lang (default "en") collapses MultiLanguageProperty cells to one language. - in operator: condition.Values + op="in" expands to an OR of eq comparisons (reuses the full field/path/numeric logic). - aas_get_submodels(identifiers[], format, select?, lang?): fetch many submodels in one call; with select it returns a table. - aas_get_shells(identifiers[], format): fetch many shells in one call. - Per-call timing: each tool logs "[MCP] <tool> done in X ms" in addition to the entry line, for performance analysis on large databases. Shared helpers BuildProjectionRow and BuildShellObject avoid duplication. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
eq/ne built a purely numeric comparison when the value parsed as a number, so eq "2908936" (a string-typed ManufacturerArticleNumber that happens to be all digits) compared against the numeric column and never matched — which also broke the new in-operator for article-number lists. eq now emits an OR of string and numeric comparison (ne emits an AND of both negations) when the value is numeric, since the valueType (xs:string vs xs:double) is unknown at query time. So eq "80" still finds a Double 80 and eq "2908936" finds the digit-only string id. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Projecting a leaf idShort like "Product_type" returned the value from an accessory subtree (Part_relation...Associated_part.Manufacturer.Product_type, e.g. a mounting adapter "UWA 130") instead of the actual product, because the leaf search returned the first match anywhere in the tree. GetElementByPath now prefers matches outside accessory branches (Part_relation / Associated_part), falling back to the first match only if the field exists exclusively under accessories. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An aas.* condition in a submodel query produces invalid SQL
("no such column: aas.IdShort"), which the engine raised as a
SqliteException that the tools did not catch -> unhandled exception and an
opaque error. Wrap QueryGetSMs in TryQuery so engine failures become a
readable {error} the model can recover from, instead of crashing the call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bare leaf idShort with a value comparison (e.g. Power_output gt 500) expanded to $and[$sme#idShort==X, $sme#value op Y], which is NOT correlated to the same element: it matched submodels that have ANY element named X AND ANY element with a value op Y -> false positives (123 instead of 16; devices with Power_output=240 but some other value>500 matched). Use the engine's correlated path mechanism instead: $or of the top-level path ($sme.<leaf>#field) and the nested wildcard path ($sme.%.<leaf>#field, % = any path prefix). Each is a single correlated path subquery (idShortPath and value in the same row). Verified: flowMax eq 350 now returns 0 (was a false positive because presMax=350 exists), flowMax eq 80 -> 1, top-level SerialNumber -> 1, numeric gt still works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aas_count previously ran the full QueryGetSMs path, which materializes every matching submodel (the slow "Collect results" phase) just to count them, and was capped at 500. On larger data this took 5-13 s and gave wrong totals above the cap. Add a dedicated count-only path that stops after the id query and returns result.Count, skipping collection entirely: - Query.GetQueryDataCount: mirrors GetQueryData setup, returns ids.Count. - EntityFrameworkPersistenceService: implement the DbRequestOp.QueryCountSMs case (was commented out) -> GetQueryDataCount. - IDbRequestHandlerService/DbRequestHandlerService.QueryCountSMs: add ResultType param (set ResultType.Submodel) so it counts submodels; updated the GraphQL CountSMs caller accordingly. - McpQueryTools.AasCount: call QueryCountSMs with pageSize=int.MaxValue (LIMIT covers all rows on SQLite & Postgres); drop the 500 cap and the now-obsolete "capped" field; wrap in try/catch for readable errors. Verified locally: aas_count == aas_query counts (7,6,1), engine logs "Count query in 4-6 ms" vs the prior multi-second collect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # src/AasxServerDB/Query.cs
The OR-distribution fallback (no security merged) did not strip a redundant enclosing paren, so a fully-wrapped top-level OR stayed one monolithic branch (full SMSets scan) instead of splitting into value-/index-driven branches. Apply StripEnclosingParens before SplitTopLevelOr in both the joined and direct union/temp builders. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A non-selective contains (e.g. 'Phoenix' on a Phoenix-Contact dataset) made the value-driven IN/FTS path materialize a huge match set (~124s). A capped FTS probe (LIMIT N over ValueSets_fts) now classifies each single leading-wildcard $sme#value contains: 'common' (>= N matches) -> correlated EXISTS (early-stop under ORDER BY/LIMIT); rare/zero -> unchanged value-driven IN/FTS (already fast). The matched pattern is passed to ApplySqliteTrigramIndex as a skip-set so the FTS candidate filter is not re-injected into the EXISTS. Cap is settable for tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add per-tool title + read-only annotations via [McpServerTool] attributes, and inject _meta in the ListTools filter: securitySchemes [{type:noauth}] (the _meta location ChatGPT reads; typed SDK cannot emit a custom top-level field) plus openai/toolInvocation/invoking|invoked status text. name/description/inputSchema and behavior unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bare $sme#value comparison now becomes its own existential over the submodel's elements (BuildOverallSqlNode routes it through the value-match builder) instead of a ValueSets join. Multiple bare value conditions under $and are emitted as separate EXISTS(A) AND EXISTS(B) (per-element semantics: different elements may satisfy each), fixing the previous wrong 0-result EXISTS(A AND B) on the same row. Extend the common-value probe (previously contains-only) to exact equality and prefix (starts-with): a capped index probe classifies a frequent value and routes it to the correlated EXISTS (early-stop under ORDER BY/LIMIT) instead of materializing the full value-driven IN set. Fixes a regression where a common '=' value with LIMIT 3 materialized its entire match set (~23 s -> ~0.4 s); a common prefix likewise (-> ~6 ms). Tests: bare-value FILTER-inside-value-match, common/rare equality, common prefix. 50/50 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add possibility to make queries with MCP server support and small refactoring for data access in CrudOperator